﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace EasyNet.TCP.Client
{
    /// <summary>
    /// 客户端核心抽象类
    /// </summary>
    abstract public class ClientCore
    {
        //非后备字段
        private readonly object[] lockers = new object[2];//线程锁
        //后备字段
        private string? _directory = null;
        private Encoding _encoding = Encoding.UTF8;
        private ConcurrentQueue<FileMsg> _sendingFiles = new ConcurrentQueue<FileMsg>();
        private ConcurrentQueue<FileMsg> _receivingFiles = new ConcurrentQueue<FileMsg>();

        abstract protected void ServerConnected();
        abstract protected void ByteArrayPKTHandled(byte[] msg_);
        abstract protected void TextPKTHandled(string msg_);
        abstract protected void FileInfoPKTHandled(FileMsg file_msg);
        abstract protected void FilePKTHandled(FileMsg file_msg);
        abstract protected void FileReceivingStarted(FileMsg file_msg);
        abstract protected void FileReceived(FileMsg file_msg);
        abstract protected void FilePKTSent(FileMsg file_msg);
        abstract protected void FileSent(FileMsg file_msg);
        abstract protected void ServerDisconnected();
        abstract protected void ServerConnectingException(Exception exception_);
        abstract protected void ReceivingException(Exception exception_);
        abstract protected void HandlingTextPKTException(Exception exception_);
        abstract protected void HandlingFileInfoPKTException(Exception exception_);
        abstract protected void HandlingFilePKTException(Exception exception_);
        abstract protected void DecryptingByteArrayPKTException(Exception exception_);
        abstract protected void DecryptingTextPKTException(Exception exception_);
        abstract protected void DecryptingFileInfoPKTException(Exception exception_);
        abstract protected void DecryptingFilePKTException(Exception exception_);
        abstract protected void FileSendingException(Exception exception_);
        abstract protected byte[] ByteArrayPKTEncryptor(byte[] data_);
        abstract protected byte[] TextPKTEncryptor(byte[] data_);
        abstract protected byte[] FileInfoPKTEncryptor(byte[] data_);
        abstract protected byte[] FilePKTEncryptor(byte[] data_);
        abstract protected byte[] ByteArrayPKTDecryptor(byte[] data_);
        abstract protected byte[] TextPKTDecryptor(byte[] data_);
        abstract protected byte[] FileInfoPKTDecryptor(byte[] data_);
        abstract protected byte[] FilePKTDecryptor(byte[] data_);

        public void Connect(string realm_name_or_ip, int server_port)
        {
            Task.Run(() =>
            {
                try
                {
                    ServerSocket = TCPTools.EasyConnect(realm_name_or_ip, server_port);
                    ServerSocket.SendBufferSize = 10 * 1024 * 1024;
                    ServerSocket.ReceiveBufferSize = 10 * 1024 * 1024;
                    ClientIP = ServerSocket.LocalEndPoint.ToString().Split(':')[0];
                    ClientPort = Convert.ToInt32(ServerSocket.LocalEndPoint.ToString().Split(':')[1]);
                    ServerIP = ServerSocket.RemoteEndPoint.ToString().Split(':')[0];
                    ServerPort = server_port;
                }
                catch (Exception exception)
                {
                    ServerConnectingException(exception);
                    return;
                }
                ServerConnected();
            });
        }
        public void Receive()
        {
            Task.Run(() =>
            {
                FileStream? fileStream = null;
                while (true)
                {
                    if (!Connected)
                    {
                        ServerDisconnected();
                        return;
                    }
                    if (ServerSocket.Available == 0)
                    {
                        continue;
                    }
                    while (ServerSocket.Available > 0)
                    {
                        byte[] type = new byte[1];
                        byte[] length = new byte[4];
                        byte[] data;
                        try
                        {
                            /*
                            ServerSocket.Receive(type, type.Length, SocketFlags.None);//接收包类型
                            ServerSocket.Receive(length, length.Length, SocketFlags.None);//接收包长度
                            int data_length = BitConverter.ToInt32(length, 0);
                            data = new byte[data_length];
                            */
                            data = new byte[ServerSocket.Available];

                            ServerSocket.Receive(data, data.Length, SocketFlags.None);//接收包数据
                        }
                        catch (Exception exception)
                        {
                            ReceivingException(exception);
                            return;
                        }
                        try
                        {
                            data = TextPKTDecryptor(data);
                        }
                        catch (Exception exception)
                        {
                            DecryptingTextPKTException(exception);
                            continue;
                        }
                        string msg;
                        try
                        {
                            msg = System.Text.Encoding.UTF8.GetString(data);
                        }
                        catch (Exception exception)
                        {
                            HandlingTextPKTException(exception);
                            continue;
                        }
                        TextPKTHandled(msg);
                        /*
                        switch (type[0])
                        {
                            case (byte)PacketType.ByteArray:
                                try
                                {
                                    data = ByteArrayPKTDecryptor(data);
                                }
                                catch (Exception exception)
                                {
                                    DecryptingByteArrayPKTException(exception);
                                    continue;
                                }
                                ByteArrayPKTHandled(data);
                                break;
                            case (byte)PacketType.Text:
                                try
                                {
                                    data = TextPKTDecryptor(data);
                                }
                                catch (Exception exception)
                                {
                                    DecryptingTextPKTException(exception);
                                    continue;
                                }
                                string msg;
                                try
                                {
                                    msg = System.Text.Encoding.UTF8.GetString(data);
                                }
                                catch (Exception exception)
                                {
                                    HandlingTextPKTException(exception);
                                    continue;
                                }
                                TextPKTHandled(msg);
                                break;
                            case (byte)PacketType.FileInfo:
                                try
                                {
                                    data = FileInfoPKTDecryptor(data);
                                }
                                catch (Exception exception)
                                {
                                    DecryptingFileInfoPKTException(exception);
                                    continue;
                                }
                                FileMsg fileMsg;
                                try
                                {
                                    byte[] fileNameLengthBytes = data.Skip(0).Take(4).ToArray();
                                    int fileNameLength = BitConverter.ToInt32(fileNameLengthBytes);
                                    byte[] fileNameBytes = data.Skip(4).Take(fileNameLength).ToArray();
                                    string fileName = Encoding.UTF8.GetString(fileNameBytes);
                                    byte[] fileSizeBytes = data.Skip(4 + fileNameLength).Take(8).ToArray();
                                    long fileSize = BitConverter.ToInt64(fileSizeBytes);
                                    fileMsg = new FileMsg(Directory, fileName, fileSize);
                                    _receivingFiles.Enqueue(fileMsg);
                                }
                                catch (Exception exception)
                                {
                                    HandlingFileInfoPKTException(exception);
                                    continue;
                                }
                                FileInfoPKTHandled(fileMsg);
                                break;
                            case (byte)PacketType.File:
                                try
                                {
                                    data = FilePKTDecryptor(data);
                                }
                                catch (Exception exception)
                                {
                                    DecryptingFilePKTException(exception);
                                    continue;
                                }
                                FileMsg fileMsg2;
                                try
                                {
                                    _receivingFiles.TryPeek(out fileMsg2);
                                }
                                catch (Exception exception)
                                {
                                    HandlingFilePKTException(exception);
                                    continue;
                                }
                                if (fileMsg2.TransmittedSize == 0)
                                {
                                    try
                                    {
                                        fileStream = new FileStream(Directory + fileMsg2.FileName, FileMode.OpenOrCreate, FileAccess.Write);
                                    }
                                    catch (Exception exception)
                                    {
                                        HandlingFilePKTException(exception);
                                        continue;
                                    }
                                    FileReceivingStarted(fileMsg2);
                                }
                                try
                                {
                                    if (fileStream != null)
                                        fileStream.Write(data, 0, data.Length);
                                }
                                catch (Exception exception)
                                {
                                    HandlingFilePKTException(exception);
                                    continue;
                                }
                                fileMsg2.TransmittedSize += data.Length;
                                FilePKTHandled(fileMsg2);
                                if (fileMsg2.TransmittedSize == fileMsg2.FileSize)
                                {
                                    if (fileStream != null)
                                        fileStream.Close();
                                    _receivingFiles.TryDequeue(out fileMsg2);
                                    fileMsg2.TransmittedSize = 0;
                                    FileReceived(fileMsg2);
                                }
                                break;
                        }
                        */
                    }
                }
            });
        }
        public void SendTextPKT(string text_)
        {
            byte[] packet = Packet.CreateTextPKT(text_, Encoding, TextPKTEncryptor);
            ServerSocket.Send(packet, packet.Length, SocketFlags.None);
        }
        public void SendByteArrayPKT(byte[] byte_array)
        {
            byte[] packet = Packet.CreateByteArrayPKT(byte_array, ByteArrayPKTEncryptor);
            ServerSocket.Send(packet, packet.Length, SocketFlags.None);
        }
        public void SendFile(string file_path, string text_msg)
        {
            FileMsg fileMsg;
            long fileSize;
            try
            {
                string fileName = Path.GetFileName(file_path);
                fileSize = new FileInfo(file_path).Length;
                byte[] fileInfoPacket = Packet.CreateFileInfoPKT(fileName, fileSize, FileInfoPKTEncryptor);
                ServerSocket.Send(fileInfoPacket, fileInfoPacket.Length, SocketFlags.None);
                SendFileAsync();
            }
            catch (Exception exception)
            {
                FileSendingException(exception);
                return;
            }
            fileMsg = new FileMsg(file_path, fileSize);
            _sendingFiles.Enqueue(fileMsg);
        }
        private void SendFileAsync()
        {
            Task.Run(() => 
            {
                while (_sendingFiles.TryPeek(out FileMsg fileMsg))
                {
                    FileStream fileStream = new FileStream(fileMsg.FilePath, FileMode.Open, FileAccess.Read);
                    byte[] file_bytes = new byte[1 * 1024 * 1024];
                    while (true)
                    {
                        try
                        {
                            int r = fileStream.Read(file_bytes, 0, file_bytes.Length);
                            if (r <= 0) break; //若是从流中读取完毕,则break;
                            byte[] filePacket;
                            if (r == file_bytes.Length)
                            {
                                filePacket = Packet.CreateFilePKT(file_bytes, FilePKTEncryptor);
                                ServerSocket.Send(filePacket, filePacket.Length, SocketFlags.None);
                                fileMsg.TransmittedSize += file_bytes.Length;
                            }
                            else
                            {
                                byte[] leave_file_bytes = file_bytes.Skip(0).Take(r).ToArray();//最后剩下的文件字节数组
                                filePacket = Packet.CreateFilePKT(leave_file_bytes, FilePKTEncryptor);
                                ServerSocket.Send(filePacket, filePacket.Length, SocketFlags.None);
                                fileMsg.TransmittedSize += leave_file_bytes.Length;
                            }
                        }
                        catch (Exception exception)
                        {
                            _sendingFiles.TryDequeue(out fileMsg);
                            fileStream.Close();
                            FileSendingException(exception);
                            continue;
                        }
                        FilePKTSent(fileMsg);
                    }
                    _sendingFiles.TryDequeue(out fileMsg);
                    fileStream.Close();
                    FileSent(fileMsg);
                }
            });
        }
        public string ServerIP { get; private set; }
        public int ServerPort { get; private set; }
        public string ClientIP { get; private set; }
        public int ClientPort { get; private set; }
        public string? Directory
        {
            get
            {
                return _directory;
            }
            set
            {
                lock (lockers[0])
                {
                    _directory = value;
                }
            }
        }
        public Encoding Encoding
        {
            get
            {
                return _encoding;
            }
            set
            {
                lock (lockers[1])
                {
                    _encoding = value;
                }
            }
        }
        public bool Connected
        {
            get
            {
                return TCPTools.SocketConnected(ServerSocket);
            }
        }
        public IReadOnlyCollection<FileMsg> SendingFiles
        {
            get
            {
                return _sendingFiles;
            }
        }
        public IReadOnlyCollection<FileMsg> ReceivingFiles
        {
            get
            {
                return _receivingFiles;
            }
        }
        private Socket ServerSocket { get; set; }
        public ClientCore() 
        {
            for (int i = 0; i < lockers.Length; i++)
            {
                lockers[i] = new object();
            }
        }
    }
}
